Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go to definition in classpath #23

Merged
merged 35 commits into from
Nov 14, 2017
Merged

Conversation

olafurpg
Copy link
Member

@olafurpg olafurpg commented Nov 10, 2017

This PR implements a Ctags module that is able to build semanticdbs for Scala and Java source files in the classpath, without any compilation or analyzing classfiles. The scala indexer uses the scalameta parser to find definitions of global symbols and record the positions at the rate of ~30k lines/s (see scalameta/scalameta#1130 for idea how to speed parsing by up to 40%). The Java indexer uses qdox which goes through java sources at ~250k loc/s on my laptop. qdox specifically designed for performance in mind and does not even parse bodies of methods or other metadata that scalameta parser collects.

We store the full file contents of all source files in the classpath in-memory. This goes against one the the core goals of this project, which is to keep memory usage as low as possible.

jump-to-classpath

stdlib

This commit makes it possible to quickly index a classpath of a project.
This makes it possible to jump to definition from a project source file
to a dependency source file. Note that this does not make is possible to
jump to definition from dependency sources to dependency sources.

While testing this feature, I extended the Jars utility to fetch
-sources jars instead of regular jars and adapted the
scala.meta.Classpath.deep method to read entries of jar files and report
errors using our logging infrastructure.
The implementation in this commit is very hacky since I'm not sure how
we can jump to definition inside entries of jars. I'm sure it's possible
to just give the jar uri or something and everything automagically
works.

We index the entire classpath for every entry in the project/config
matrix without any deduplication. This needs to be optimized.

Most of the work was to get the compiler to correctly download the
sources jars from the classpath. I had to wrap the call to `Jars.fetch`
inside of a `Future` to unblock the compiler for future updates.
This is a bug that needs to be fixed, we should not be randomly
sprinkling blocks inside `Future  { ... }`.
Previously we would re-index the same jars for every project/config and
on every configuration change. Now we only index each jar once.
The positions are off and this implementation doesn't handle static
methods.
It does not scale to have ctags indexing for all languages in a single
file. The commit abstracts over the commonalities between scala and java
indexing so that we can reuse most code between the two.
I've started getting MetaspaceErrors and I suspect they're related to us
not properly cleaning up resources. Maybe this will fix the problem.
@olafurpg
Copy link
Member Author

olafurpg commented Nov 11, 2017

Go to definition for java sources in the classpath works!

javajump

It doesn't handle all cases like static methods or inner classes. JDK sources are not indexed either, I think we have to special case it. However, this is a very promising start, as long as we can move the editor to a java source file we can delegate the rest to the Java LSP.

I started getting MetaSpaceErrors, which is not good. The last commit adds one missing .close() which I hope solves the problem, if not we need to figure out what is causing those since it's very annoying to have to restart sbt every few minutes (and we don't want the server leaking memory either!)

@olafurpg
Copy link
Member Author

The JavaCtags module is quite primitive for now. I struggled to get nice positions for names, it seems they were all offsets. The infrastructure is there however to index java sources. In fact, we can add any other language for that matter! It should be possible for example to support go to definition in protobuf/thrift/avro sources.

@olafurpg olafurpg requested a review from gabro November 11, 2017 13:26
def index(filename: String, contents: String): Document = {
val input = Input.VirtualFile(filename, contents)
val tree = {
import scala.meta._
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 13-24 import various bits of scala.meta, then here you import all. Why? Is the style no wildcards allowed at top of file?

import java.nio.file.Files
import java.util.Properties
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What caused this to be repeated 3 times? Some automated tool?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with OrganizeImports

extends LazyLogging {
private val documentPubSub =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name makes me think it is (Publisher, Subscriber) but based on the usage it is actually (Subscriber, Publisher)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Previous we could still overindex in race conditions, now we use
Map.computeIfAbsent to properly make sure we only index each jar once.
Race conditions are very possible since multiple .compilerconfig are
often created at the same time.
package scala.meta.languageserver

import java.io.File
import java.nio.file.Files
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using IO and NIO?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java.io is only for File.pathSeparator, otherwise we use nio for I/O work

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIO Filesystem has a platform independent separator if you want to stick with the NIO api.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I prefer java.io.File.pathSeparator over FileSystems.getDefault.getSeparator

Copy link
Member Author

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review @ShaneDelmore !

import java.nio.file.Files
import java.util.Properties
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with OrganizeImports

extends LazyLogging {
private val documentPubSub =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

package scala.meta.languageserver

import java.io.File
import java.nio.file.Files
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java.io is only for File.pathSeparator, otherwise we use nio for I/O work

)
}

}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative suggestion in a completely different style. Nothing wrong with this one though if you want to keep it. Ignore my lack of filename in the IllegalArgumentExceptions, I just whipped this up in a worksheet.

def pos(content: String, line: Int, col: Int): Int = {
  val (lineCount, pos) = content
    .linesWithSeparators
    .take(line + 1)
    .foldLeft((0, 0)){ (accum, str) =>
      val (lineNum, charCount) = accum
      if (lineNum < line) {
        (lineNum + 1, charCount + str.length)
      } else {
        require(str.length >= col, "Line is not long enough")
        (lineNum + 1, charCount + col)
      }
    }
  require(lineCount == line + 1, "Not enough lines")
  pos
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is on a fairly critical path since it's run for every start/end point during completions. foldLeft + tuples creates a lot of unnecessary allocations compared to the while loops.

However, I just found out that scala.meta.Input has an internal private lazy val cachedLineIndices: Array[Int] which allows you to get the offset of a line in from an array lookup. I've refactored to use this instead of rolling our own.

@@ -27,6 +27,7 @@ object ScalametaEnrichments {
}
}
implicit class XtensionInputLSP(val input: m.Input) extends AnyVal {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidental new line?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

- static classes
- static methods
- handle enums in java
- reuse test suite for java/scala
*
* Observable[Unit] is not descriptive of what the observable represents.
* Instead, we create Unit-like types to better document what effects are
* flowing through our application.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poor man's type-safety. I refactored to this in fact because I hit on bugs related to accidentally creating Observable[Observable[T]] from a .map when I should have been using .flatMap to build Observable[Unit]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these phantom types?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really since those values also exist at runtime. I'm still not sure if this was necessary, maybe we can avoid it by using stricter scalac options (no discard unit). However, I think it's a nice replacement for Observable[Unit].

The JDK needs to be special handled since it's included in the classpath
by default and does not appear in the `updateClassifiers` report from
sbt. The JDK on my machine contains ~2.5M loc but still indexes in ~8s
at a staggering rate of ~450k loc/s. The need to persist these indices
across editor sessions grows bigger.
Copy link
Member

@gabro gabro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went through the whole PR and I think I understand the most of it. Very nice work @olafurpg!

I've left some comments and questions, but overall I think we can merge this and improve things in separate issues.

I think the greatest improvements (maybe for a future PR) would be:

  • preload .compilerconfig files (easy)
  • make the jump to sources less hacky (without copying the file). I don't fully understand what Java LSP does, but that seems LSP-compliant

def classloadScalafmt(version: String, out: PrintStream): Formatter = {
val urls = Jars
.fetch("com.geirsson", "scalafmt-cli_2.12", version, out)
.iterator
.map(_.toURI.toURL)
.toArray
out.println(s"Classloading scalafmt with ${urls.length} downloaded jars")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you printing to out instead of using the logger?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is actually run before we redirect stdout, so if we use logger we it crashes the vscode client. However, I clearly should just redirect stdout before loading scalafmt 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, removed out parameter.

def fetch(
modules: Iterable[ModuleID],
out: PrintStream,
sources: Boolean
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bikeshedding here, but sources isn't very descriptive. How about: fetchSources or shouldFetchSources, or similar?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, fetchSourceJars.

logger.info(s"File $path changed, extension=$name")
name match {
case "semanticdb" => semanticdbSubscriber.onNext(path)
case "compilerconfig" => compilerConfigSubscriber.onNext(path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also preload all existing compilerconfigfiles at launch, similarly to what we do with semanticdb files?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do actually, since we call this method from loadAllSemanticdbs... method, I've renamed the method to be more descriptive.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB, we should really run that method asynchronously from the initialize method since it slows down server startup.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, in general I think initialize should return immediately and run the rest of the work asynchronously. We can even dynamically register for capabilities when the single services finish initializing.

def check(filename: String, original: String, expected: String): Unit = {
test(filename) {
val obtained = Ctags.index(filename, original)
println(obtained)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgot a println

class ClasspathCtagsTest extends FunSuite with DiffAssertions {

// NOTE(olafur) this test is a bit slow since it downloads jars from the internet.
test("index classpath") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we group this under a ci-slow target to run separately (like we do in Scalafix?)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of a CI slow category as well.

ctags.Ctags.index(sourcesClasspath) { doc =>
documentSubscriber.onNext(doc)
}
import scala.collection.JavaConverters._
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this import for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unused, removed.

sourceJars: List[AbsolutePath]
) {
override def toString: String =
s"CompilerConfig(" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we have pprint as a dependency, you can probably just do pprint.stringify(this)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find pprint.stringify, it seems to have been removed in 0.5. I don't think pprint includes the names of the fields. This string appears in the logs whenever a new presentation compiler is loaded so I customized it to make it compact and readable (full classpath can be huge).

MulticastStrategy.Publish,
OverflowStrategy.ClearBuffer(2)
)
Observable.multicast[AbsolutePath](MulticastStrategy.Publish)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the overflow strategy gone?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it without knowing what it does. I think we should use the default unless we know what we're doing.

// However, that is a vscode only solution and we'd like this work for all
// text editors. Therefore, we write instead the file contents to disk in order to
// return a file: uri.
private def createFileInWorkspaceTarget(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as a comparison, the Java LSP jumps to a .class file containing the source.
I'm not sure I understand the implementation, but here's the relevant bit https://github.com/eclipse/eclipse.jdt.ls/blob/2127f12fb3700dd783b75d973e4f2ecc134b9ddf/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToDefinitionHandler.java#L52

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find! I opened #36 to track this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added note in code.

def index(input: Input.VirtualFile): Document = {
logger.trace(s"Indexing ${input.path} with length ${input.value.length}")
val indexer: CtagsIndexer =
if (isScala(input.path)) ScalaCtags.index(input)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what would it take to index .sbt files?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not much really, I can give some pointers if you open an issue 😉

@olafurpg olafurpg mentioned this pull request Nov 14, 2017
- Move overly deeply nested methods defined inside indexRoot(). Diff is
huge, but it's only indentation.
- Catch NPE which happens in the JDK, don't want it to appear in the
  logs every time you start the language server.
Previously the logs contains a ton of "File ...*.class changed", which
is not useful.
The handling of default fields was apparently wrong, so I skipped the
tests. I also skip the slow tests that do heavy IO. We can consider
adding a IntegrationTest or separate testsSlow project, but for now
I want snappy tests and a small build.
Copy link
Member Author

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the detailed review @gabro this PR became way too big, I kept adding more stuff to it while waiting for the lsp4j port. I will keep the PRs smaller from now on 😄

Some unattended changes

build.sbt Outdated
"io.get-coursier" %% "coursier" % coursier.util.Properties.version,
"io.get-coursier" %% "coursier-cache" % coursier.util.Properties.version,
"ch.epfl.scala" % "scalafix-cli" % "0.5.3" cross CrossVersion.full,
"com.geirsson" %% "scalafmt-core" % "1.3.0"
"org.scalatest" %% "scalatest" % "3.0.3" % "test",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, this was just copy pasted from above, I normally use Test

ctags.Ctags.index(sourcesClasspath) { doc =>
documentSubscriber.onNext(doc)
}
import scala.collection.JavaConverters._
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unused, removed.

sourceJars: List[AbsolutePath]
) {
override def toString: String =
s"CompilerConfig(" +
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find pprint.stringify, it seems to have been removed in 0.5. I don't think pprint includes the names of the fields. This string appears in the logs whenever a new presentation compiler is loaded so I customized it to make it compact and readable (full classpath can be huge).

*
* Observable[Unit] is not descriptive of what the observable represents.
* Instead, we create Unit-like types to better document what effects are
* flowing through our application.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really since those values also exist at runtime. I'm still not sure if this was necessary, maybe we can avoid it by using stricter scalac options (no discard unit). However, I think it's a nice replacement for Observable[Unit].

def classloadScalafmt(version: String, out: PrintStream): Formatter = {
val urls = Jars
.fetch("com.geirsson", "scalafmt-cli_2.12", version, out)
.iterator
.map(_.toURI.toURL)
.toArray
out.println(s"Classloading scalafmt with ${urls.length} downloaded jars")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is actually run before we redirect stdout, so if we use logger we it crashes the vscode client. However, I clearly should just redirect stdout before loading scalafmt 😅

logger.info(s"File $path changed, extension=$name")
name match {
case "semanticdb" => semanticdbSubscriber.onNext(path)
case "compilerconfig" => compilerConfigSubscriber.onNext(path)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB, we should really run that method asynchronously from the initialize method since it slows down server startup.

// However, that is a vscode only solution and we'd like this work for all
// text editors. Therefore, we write instead the file contents to disk in order to
// return a file: uri.
private def createFileInWorkspaceTarget(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find! I opened #36 to track this.

// However, that is a vscode only solution and we'd like this work for all
// text editors. Therefore, we write instead the file contents to disk in order to
// return a file: uri.
private def createFileInWorkspaceTarget(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added note in code.

def index(input: Input.VirtualFile): Document = {
logger.trace(s"Indexing ${input.path} with length ${input.value.length}")
val indexer: CtagsIndexer =
if (isScala(input.path)) ScalaCtags.index(input)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not much really, I can give some pointers if you open an issue 😉

|_root_.sourcecode.Util.getName. => def getName
|_root_.sourcecode.Util.isSynthetic. => def isSynthetic
|_root_.sourcecode.Util.isSyntheticName. => def isSyntheticName
""".stripMargin
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YES I WANT DIS IN SCALA 🙏

@olafurpg
Copy link
Member Author

MERGING 🚀

@olafurpg olafurpg merged commit 8bbef8e into scalameta:master Nov 14, 2017
@olafurpg olafurpg deleted the classpath branch November 14, 2017 20:11
@olafurpg
Copy link
Member Author

#35 is probably the biggest priority right now, since as you may notice, starting the server will always trigger reindexing the full JDK, even for trivial projects.

@laughedelic
Copy link
Member

Does it happen on initialization or on the first definition request?

@gabro
Copy link
Member

gabro commented Nov 14, 2017

I think when compilerconfig is detected, so on initialization

@olafurpg
Copy link
Member Author

It's triggered during initialization, but it does not block server initialize.

@laughedelic
Copy link
Member

OK

object ScalaCtags {
def index(input: Input.VirtualFile): CtagsIndexer = {
val root: Source = input.parse[Source].get
new Traverser with CtagsIndexer {
Copy link
Member

@laughedelic laughedelic Nov 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olafurpg I was reading this code and I find it very similar to what I wanted to do for symbols outline. I started implementing something similar to this to traverse the tree just for the "global" symbols, but then I saw this and I'm wondering where is this Traverser from and how to use it. Direct search in Scalameta repo didn't give me a definition (I'm completely lost in Scalameta's project structure 🙁 )

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laughedelic I don’t have a computer in front of me to verify this but I believe some of the bits that you can’t tell where they are defined from scalameta are actually bits of the compiler plugin. Scala has a standard format for compiler plugins where they hook into a global content and supply their own traversed that the compiler uses to walk the ast and pull information out. It can appear a bit magic because it’s not fully scalameta code, but partially code for the plugin framework. I think traverser is one of these things.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, see this scala plugin demo project. Every one I have seen folllows a similar structure. https://github.com/cb372/scalac-plugin-basic/blob/master/src/main/scala/basic/BasicPlugin.scala#L56

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I think of scalameta as being intertwined with scalac so I don’t have to be, but when trying to understand scalameta source you will end up having to dig into scalac source as well as it is translating between the two ASTs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShaneDelmore thanks a lot! This is quite enlightening. I didn't know anything about it. I'm not sure if I want to dig in the scalac's source just yet, but how can one use Scalameta without reading its sources? "_

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laughedelic I think Traverser is generated with macro annotations, it's essentially one big boilerplate pattern match on an ADT with 100s of nodes. Same for Transformer. The Traverser contract is quite simple however, override apply, do somthing with the tree node, optionally continue visiting children with super.apply(tree).

.collect is for example implemented on top of Traverser

Copy link
Member Author

@olafurpg olafurpg Nov 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scalameta doesn't convert between compiler data structures, the scalameta module doesn't depend on scala-compiler in fact. However, scalameta (ab)uses macro annotations so a lot of the source code is hidden inside quasiquotes to be expanded at compile time. To understand the macros it's helpful to know a bit about scalac

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laughedelic Here's how I picked up things about meta

@ import $ivy.`org.scalameta::contrib:2.0.0`, scala.meta._, contrib._
import $ivy.$                             , scala.meta._, contrib._

@ q"trait A { val x: Int }"
res1: Defn.Trait = Defn.Trait(
  List(),
  Type.Name("A"),
  List(),
  Ctor.Primary(List(), _, List()),
  Template(List(), List(), Self(_, None), List(Decl.Val(List(), List(Pat.Var(Term.Name("x"))), Type.Name("Int"))))
)

https://astexplorer.net/#/gist/104048fb30df84e64a3e46d774f26b0f/f4b78594d5b1716f5d2821ef76e8d48b3fafc8ba

writing new docs on more detailed parts like Transformer/Traverser is on my TODO!

Copy link
Member

@laughedelic laughedelic Nov 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olafurpg hey! thanks a lot for explaining the things. I'm using the same "exploratory" approach too, it's quite enough for simple uses.
I just often wonder what is available in general (not to reinvent the wheel): normally if there's a generated scaladoc, it may be enough or (more often) I just check the sources and see the "global symbols outline" 😄 but with this "macrofull" code it's kind of hard (I'm also very superficially familiar with macros).
Another thing that bothers me is that I don't understand how code is organized in the scalameta repo. Probably there is some development dcoumentation for it that I didn't find?

Copy link
Member

@laughedelic laughedelic Nov 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShaneDelmore btw, thanks for mentioning quasiquotes.md, I didn't notice it before 👍 (I had Trees.scala open instead 😅 )

case t: Defn.Object => term(t.name, OBJECT); Continue
case t: Defn.Def => term(t.name, DEF); Stop
case Defn.Val(_, Pat.Var(name) :: Nil, _, _) => term(name, DEF); Stop
case Defn.Var(_, Pat.Var(name) :: Nil, _, _) => term(name, DEF); Stop
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olafurpg What about declarations? For example if we have somewhere defined

trait T { 
  type X
  val x: X 
}

X will be a Decl.Type and x a Decl.Val, so if later in the code we encounter something like def foo(t: T): t.X and want to lookup that t.X... No, this example is bad, but I think you got the idea. If not, I'll think of a better example, sorry 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are actually not handled! This is a bug 😅

Copy link
Member

@laughedelic laughedelic Nov 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'll try to add it then.

Another question here: as I see from the definition of those term/type indexer methods they just use the name position, but for the outline we need the definition position instead. Do you think it could be also useful for the general indexer to use that wider position range? e.g.

case t: Defn.Def => term(t.name, t.pos, DEF)

Symbols outline would be then just a simple reindexing of the document on every documentSymbol request.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it could be also useful for the general indexer to use that wider position range? e.g.

Yes! I've been considering refactoring the indexer to construct a custom data structure that we can tweak for our needs. Then we can reuse it for multiple features like outline + go to definition.

Can you open a ticket to discuss details?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants